package geneticpolyimage;

import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.Comparator;
import java.util.Vector;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;

/**
 * Class solver provide main method to solve problem
 * 
 * @author igor & mattia
 */
public class Solver extends Thread {

    public int gen = 0;
    public int popsize = 100;
    public int nBest = 0;
    public int nDie = 10;
    private double maxFitness;
    private double minFitness;
    private double meanFitness;
    public double mutrate = 0.2;
    public double crossrate = 1;
    public Problem problem = null;
    public GeneticOp genop = null;
    public boolean solved = false;
    public Vector<Individual> population = new Vector<Individual>();
    private boolean done = false;
    private Individual thebest = null;
    private int crossType = 1;
    private int selectionMethod = 0;
    private int howmanypoly;
    private int numvertices;
    private int maxx;
    private int maxy;
    private String imagespath = null;

    /**
     * When called sets done variable true
     * to break the main thread
     */
    public void done() {
        done = true;
    }

    /**
     * Solver cosnstructor with preloaded
     * problem and default parameter
     *
     * @param problem represent problem to solve
     */
    public Solver(Problem problem) {
        this.problem = problem;
        maxx = problem.width;
        maxy = problem.height;
    }

    /**
     * Set crossover rate
     * @param r crossover rate 0...1
     */
    public void setCrossRate(double r) {
        genop.setCrossRate(r);
    }

    /**
     * Set mutation rate
     * @param r mutation rate 0...1
     */
    public void setMutRate(double r) {
        genop.setMutRate(r);
    }

    /**
     * Solver costructor
     * @param problem problem rapresenting target image to reproduce
     * @param imgpath path to store temporary image
     * @param pop population size
     * @param poly number of polygon
     * @param vertex number of vertices
     * @param mRate mutation rate
     * @param cType crossover type
     * @param nDiePercent % of individual to remove each generation
     * @param selMethod selection method
     * @param cRate crossover rate
     */
    public Solver(Problem problem, String imgpath, int pop, int poly, int vertex, double mRate, int cType, int nDiePercent, int selMethod, double cRate) {
        this.problem = problem;
        this.imagespath = imgpath;
        this.popsize = pop;
        this.howmanypoly = poly;
        this.numvertices = vertex;
        this.mutrate = mRate;
        this.crossrate = cRate;
        this.crossType = cType;
        this.selectionMethod = selMethod;
        nDie = ((int) (pop / 2 * nDiePercent / 100) * 2);

        maxx = problem.width;
        maxy = problem.height;
    }

    @Override
    /**
     * main thread method, initialize population and
     * evolve the population until solution found or
     * thread stopped externally
     
     */
    public void run() {
        this.initializePopulation();
        boolean sol = false;
        sol = this.checkfitness(this.population, this.problem);
        while (!sol && !done) {
            sol = this.nextGeneration();
        }
    }

    /**
     * Calculate the next generation based on actual
     * population
     */
    public boolean nextGeneration() {
        gen++;
        Vector<Individual> newPopulation = new Vector<Individual>(popsize);
        this.populationSort(population);
        if (imagespath!=null) {
            if (!this.selectBest(this.population).equals(thebest)) {
                try {
                    File file = new File(imagespath + "/" + Long.toString(System.currentTimeMillis()) + ".png");
                    ImageIO.write(this.selectBest(this.population).getImage(), "png", file);
                } catch (IOException ex) {
                    Logger.getLogger(Solver.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
        }
        thebest = (Individual) this.selectBest(this.population).clone();
        //Roulette Wheel
        for (int i = 0; i < nDie; i++) {
            selectWorst(population);
        }
        // generate intermedie population based on roulette wheel
        Vector<Individual> intermediePopulation = new Vector<Individual>(popsize);
        while (intermediePopulation.size() < popsize) {
            Individual ind = null;
            if (selectionMethod == 1) {
                ind = (Individual) selectRank(population).clone();
            } else if (selectionMethod == 2) {
                ind = (Individual) selectRoulette(population).clone();
            }
            intermediePopulation.add(ind);
        }
        // generate offspring
        while (intermediePopulation.size() > 0) {
            Individual i1 = (Individual) selectRandom(intermediePopulation).clone();
            Individual i2 = (Individual) selectRandom(intermediePopulation).clone();

            genop.mutate(i1);
            genop.mutate(i2);

            Individual[] sons = genop.crossover(i1, i2);

            newPopulation.add(sons[0]);
            newPopulation.add(sons[1]);
        }
        newPopulation.add(thebest);
        population = (Vector<Individual>) newPopulation.clone();
        return checkfitness(population, problem);
    }

    /**
     * Return current satus of evolution
     *
     * @return information of current status
     */
    public Status getStatus() {
        Status status = new Status();
        status.generation = gen;
        status.maxFitness = maxFitness;
        status.minFitness = minFitness;
        status.meanFitness = meanFitness;
        status.solved = solved;
        status.thebest = thebest;
        return status;
    }

    /**
     * Initialize population with random individue
     */
    public void initializePopulation() {
        done = false;
        genop = new GeneticOp(mutrate, crossType, crossrate);
        for (int i = 0; i < popsize; i++) {
            Individual u = new Individual(howmanypoly, numvertices, maxx, maxy);
            population.add(u);
        }
    }

    /**
     * get the sum of fitnes of current population
     *
     * @param population
     * @return totoal fitness of population
     */
    private double getSumFitness(Vector<Individual> population) {
        double totfit = 0;
        for (int i = 0; i < population.size(); i++) {
            totfit += population.get(i).fitness;
        }
        return totfit;
    }

    /**
     * select best individial based on his fitness
     *
     * @param population population to analyze
     * @return best individual
     */
    public Individual selectBest(Vector<Individual> population) {
        return population.lastElement();
    }

    /**
     * select worst individual based on his fitness
     * and remove it from the population
     *
     * @param population population to analyze
     * @return worst individual
     */
    private Individual selectWorst(Vector<Individual> population) {
        return population.remove(0);
    }

    /**
     * Select individual based on Roulette Wheel Selection
     *
     * @param population population to analyze
     * @return selected individual
     */
    private Individual selectRoulette(Vector<Individual> population) {
        int elemToRemove = 0;
        double totfit = getSumFitness(population);
        double r = (Math.random() * totfit);
        double s = 0;
        for (int i = population.size() - 1; i >= 0; i--) {
            s += population.get(i).fitness;
            if (s >= r) {
                elemToRemove = i;
                break;
            }
        }
        return population.elementAt(elemToRemove);
    }

    /**
     * Select individual based on Rank Selection
     *
     * @param population population to analyze
     * @return selected individual
     */
    private Individual selectRank(Vector<Individual> population) {
        int totfit = (1 + population.size()) * (population.size() / 2);
        int r = (int) (Math.random() * totfit);
        int elemToRemove = (int) (Math.sqrt(r * 8 + 1) - 1) / 2;
        return population.elementAt(elemToRemove);
    }

    /**
     * Print poluation
     */
    public void printPop() {
        for (int i = 0; i < population.size(); i++) {
            for (int j = 0; j < howmanypoly; j++) {
                population.get(i).polygons[j].print();
            }
        }
    }

    /**
     * Sort population
     *
     * @param population population to sort
     */
    private void populationSort(Vector<Individual> population) {
        Collections.sort(population, new Comparator() {

            public int compare(Object a, Object b) {
                return (new Double(((Individual) a).fitness)).compareTo(new Double(((Individual) b).fitness));
            }
        });
    }

    /**
     * select random individial and remove it from the population
     *
     * @param population population to analyze
     * @return selected individual
     */
    private Individual selectRandom(Vector<Individual> population) {
        int elemToRemove = (int) (Math.random() * population.size());
        return population.remove(elemToRemove);
    }

    /**
     * check fitness of population individuals
     *
     * @param population population to check
     * @param problem problem to check
     * @return true or false based on solution of the problem
     */
    public boolean checkfitness(Vector<Individual> population, Problem problem) {
        double fMax = 0;
        double fMin = Double.MAX_VALUE;
        double fMean = 0;
        for (int i = 0; i < population.size(); i++) {
            double fit = population.get(i).fitness(problem);
            fMean += fit;
            if (fit > fMax) {
                fMax = fit;
            }
            if (fit < fMin) {
                fMin = fit;
            }
        }
        maxFitness = fMax;
        minFitness = fMin;
        meanFitness = fMean / population.size();
        return solved;
    }
}
